Exkurs 1: Eine Matrix als Liste von Listen

Nicht selten müssen wir Daten in Form einer Matrix, bestehend aus Zeilen und Spalten, darstellen. Im Bereich von ABM können wir über solche Matrizen z.B. zweidimensionale “Landschaften” (sogenannte Grids), auf welchen sich unsere Agenten bewegen, darstellen. Z.B. könnte man sich vorstellen, dass das unten dargestellte Grid eines Segregationsmodells intern als Matrix dargestellt ist. Jede Zelle des Grids könnte dann als ein Feld in einer Matrix repräsentiert werden. Jedes Feld wiederum könnt durch eine Zahl, welche für eine der Farben steht, repräsentiert werden.

from matplotlib import pyplot as plt
%matplotlib inline
fig = plt.figure()
fig.set_size_inches(8,8)
plt.imshow(plt.imread('img/schelling.png'))
plt.show()
../_images/ABM_mit_Python_07a_Input_Matplotlib_1_0.png

Wie können wir solche Matrizen in Python darstellen? Es gibt in Python zahlreiche spezielle Datentypen für Matrizen, z.B. aus dem Modul Numpy das sogenannte array oder aus dem Modul Pandas der sogenannte DataFrame. Da jedoch jedes neue Modul gleichzeitig auch das Erlernen der jeweils etwas eigenen “Syntax” und sonstiger Besonderheiten dieses Moduls bedeutet, sollte man Module vor allem als Programmierneuling nur dann benutzen, wenn man sie wirklich braucht.

Tatächlich kann man eine Matrix auch relativ einfach mit den Python-Basics darstellen: als Liste von Listen. Innerhalb einer Liste befinden sich dabei Unterlisten, welche die Zeilen einer Matrix repräsentieren. Innerhalb jeder Unterliste befinden sich Elemente, deren Position jeweils einer Spalte der Matrix entspricht.

Im Folgenden ein Beispiel für eine Matrix mit 3 Zeilen und 3 Spalten. Um eine solche Matrix darzustellen, benötigt man eine Liste, mit 3 Unterlisten (Zeilen), welche wiederum jeweils 3 Elemente (Spalten) enthalten.

matrix = [ [1, 2, 3], [4, 5, 6], [7, 8, 9] ]

Damit das ganze auch etwas mehr nach Matrix aussieht, kann man dieselbe Liste bzw. Matrix auch so hinschreiben:

matrix = [ 
    [1, 2, 3], 
    [4, 5, 6], 
    [7, 8, 9]
]

Möchte man nun auf ein Element der Liste zugreifen, müssen wir zunächst per Listenindizierung auf die Unterliste (Zeile) und dann auf die entsprechende Position in der Unterliste (Spalte) zugreifen. Möchte ich z.B. auf das Element in der ersten Zeile und der ersten Spalte zugreifen, muss ich zunächst auf die erste Zeile mit dem Index 0 zugreifen:

matrix[0]
[1, 2, 3]

Um dann in dieser Zeile bzw. Liste auf die erste Spalte bzw. Position zuzugreifen, muss ein weiteres mal der Index 0 verwendet werden:

matrix[0][0]
1

Um z.B. auf das Elemente in der zweiten Zeile und der dritten Spalte zuzugreifen, müssen entsprechend der Zeilen-Index 1 und der Spalten-Index 2 angegeben werden:

matrix[1][2]
6

Die allgemeine Syntax für den Zugriff auf die Elemente einer Matrix könnte man also so beschreiben:

matrix[ZEILE][SPALTE]

Matrizen per For-Loop erstellen

Kleine Matrizen kann man noch relativ gut per Hand erstellen. Manchmal muss man auch solch “individuelle” Matrizen erstellen, dass man um die Erstellung dieser per Hand nicht drumherum kommt. Oft können wir die Erstellung von Matrizen aber per For-Loop automatisieren:

# Anzahl der Zeilen und Spalten festlegen
n_rows = 10
n_cols = 10

# Oberliste für Matrix erstellen
matrix = []

# für die Anzahl der Zeilen
for i in range(n_rows):
    
    # Unterliste für Zeile erstellen
    zeile = []
    
    # für die Anzahl der Spalten
    for j in range(n_cols):
        
        # An die Zeile ein Element anhängen (In diesem Fall eine 0)
        zeile.append(0)
    
    # Zeile (Unterliste) an Matrix (Oberliste) anhängen
    matrix.append(zeile)
matrix
[[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]

Exkurs 2: Mehrfachzuweisung bei der Erstellung von Variablen

Wenn wir bisher Variablen definiert haben, dann haben wir in jeder Zeile immer genau ein Objekt/Wert einem Variablennamen zugewiesen. Z.B. so:

x = 1
y = 2

Es gibt in Python aber auch die Möglichkeit mehrere Variablenzuweisungen in einer Zeile vorzunehmen. Dazu schreibt man mehrere Variablennamen mit einem Komma getrennt links neben das Gleichheitszeichen und genau so viele Objekte/Werte rechts neben das Gleichheitszeichen. Der erste Wert wird dann dem ersten Variablennamen zugewiesen, der zweite Wert dem zweiten Variablennamen usw. …

Statt

x = 1
y = 2

könnte man also auch

x, y = 1, 2

schreiben. Das Ergebnis ist dasselbe. Die 1 wird dem x und die 2 dem y zugewiesen. Es ist nicht wirklich zu empfehlen, eine solche Schreibweise standardmäßig zu benutzen, da die Leserlichkeit des Codes eher verringert wird. In manchen Situationen kann es dennoch praktisch sein.

Ein sinnvoller Einsatz ist die Mehrfachzuweisung v.a., wenn eine Funktion mehrere Ausgabewerte d.h. mehrere Objekte zurückgibt und wir alle ausgegebenen Objekte in eigenen Variablen speichern wollen.

Unten definiere ich die Funktion random_coordinates(), welche zwei Zufallswerte zwischen 0 und 100 zurückgibt. Diese beiden Werte weise ich dann innerhalb einer Zeile den beiden Variablennamen x_pos und y_pos zu.

import random

def random_coordinates():
    random_x = random.randint(0,100)
    random_y = random.randint(0,100)
    return random_x, random_y         # Zwei Ausgabewerte

x_pos, y_pos = random_coordinates()   # Beide Ausgabewerte jeweils eigener Variable speichern
x_pos
34
y_pos
79

Ein weiteres Einsatzgebiet von Mehrfachzuweisungen ist das relativ unkomplizierte Entpacken von kleineren Listen und anderen Datencontainern. Haben wir rechts neben dem Gleichheitszeichen z.B. eine Liste mit drei Elementen stehen und links drei kommagetrennte Variablennamen, dann werden die drei Elemente aus der Liste heraus den drei Variablennamen zugeordnet:

wuschel, pünktchen, mimi  = ["hund", "katze", "maus"]
wuschel
'hund'
pünktchen
'katze'
mimi
'maus'

Das Ganze funktioniert auch, wenn Datencontainer von Listen zurückgegeben werden. Unten gibt die Funktion random_coordinates() die Werte innerhalb einer Liste zurück, wobei die Werte der Liste direkt per Mehrfachzuweisung “entpackt” werden:

def random_coordinates():
    random_x = random.randint(0,100)
    random_y = random.randint(0,100)
    return [random_x, random_y]       # Liste als Ausgabewert

x_pos, y_pos = random_coordinates()   # Ausgegebene Liste direkt entpacken
x_pos
72
y_pos
37

Übrigens: Genaugenommen entpackt die Mehrfachzuweisung eigentlich immer einen Datencontainer, auch wenn wie hier

x, y, z = 1, 2, 3

auf der rechten Seite die Objekte nur per Komma getrennt aufgereiht werden. Denn werden Objekte in der Form wie rechts neben dem Gleichheitszeichen hingeschrieben, erstellt Python ein sogenanntes Tupel. Das ist ähnlich, wie eine Liste, ein Datencontainer, in dem alle möglichen Objekte gespeichert werden können. Der Unterschied ist nur, dass nach der Erstellung eines Tupels dieses nicht mehr verändert, also kein Element ausgetauscht werden kann. Ein erstelltes Tupel erkennt man in Python an runden Klammern:

1, 2, 3
(1, 2, 3)

Das Tupel (1, 2, 3) kann auch direkt mit runden Klammern erstellt werden:

(1, 2, 3)
(1, 2, 3)

Da aber auch der Code 1, 2, 3 das Tupel (1, 2, 3) erstellt, entpackt der Code

x, y, z = 1, 2, 3

also genaugenommen das Tupel

(1, 2, 3)

und weist dessen Elemente jeweils einer Variable zu.

Matplotlib

Matplotlib bietet verschiedene “Zugänge”, um die Diagramme von Matplotlib zu erstellen. Hier findest du einen Überblick über die 2 bzw. 3 Zugangsmöglichkeiten zur Diagrammerstellung mit Matplotlib.

Bisher haben wir eine einfache und schnelle Variante des Plottens mit Matplotlib bzw. Pyplot gewählt (die sogenannte pyplot API. Diese Form der Verwendung von Matplotlib reicht oft auch aus, um einfache Graphiken zu erstellen. Wir hätten tatsächlich auch so noch alles mögliche einstellen oder so etwas wie Beschriftungen, Legenden usw. hinzufügen können. Dennoch stößt dieser Ansatz an seine Grenzen, möchte man die “volle Kontrolle” beim Erstellen von Diagrammen haben. Und vor allem, wenn man fortgeschrittene Techniken, wie die Animation von Diagrammen anwenden, müssen wir die in diesem Kapitel behandelte Form der Anwendung von Matplotlib verwenden.

Während wir vorher allein Funktion von Matplotlib zur Erstellung der Diagramme verwendet haben, verwenden wir nun eine eher “objektorientierte” Variante der Verwendung von Matplotlib (die sogenannte object-oriented API). Wir arbeiten nun explizit mit den Objekten, aus denen die Diagramme in Matplotlib, bestehen. Denn jedes Diagramm besteht in Matplotlib aus mehreren Einzelteilen. Als wir einfach plt.plot() verwendet haben, wurden diese Einzelteile automatisch im Hintergrund erstellt. Wir erstellen diese ab jetzt selbst.

Die grundlegenden Bestandteile eines Diagramms in Matplotlib sind das sogenannte Figure-Objekt und das sogenannte Axes-Objekt.

(Eine Anmerkung: Im Folgenden werde ich immer plt. vor die Befehle schreiben, wenn es sich um Funktionen/Objekte des Moduls matplotlib.pyplot handelt.)

Figure

Eine Figure-Objekt ist das Fenster, in das ein oder mehrere Diagramme eingezeichnet werden. Es ist praktisch eine leere Leinwand, auf die prinzipiell gezeichnet werden kann. Jedes Diagramm muss auf ein Figure-Objekt gezeichnet werden.

Ein Figure-Objekt kann man u.a. mit dem Befehl plt.figure() erstellen. Ein leeres Figure-Objekt sieht wie unten im Screenshot aus (nicht vom Code irritieren lassen, diesen verwende ich nur, um den Screenshot darzustellen). Auf die große weiße Fläche könnten dort alle möglichen Diagramme eingezeichnet werden.

from matplotlib import pyplot as plt
%matplotlib inline
fig = plt.figure()
fig.set_size_inches(10,10)
plt.imshow(plt.imread('img/Figure.png'))
plt.show()
../_images/ABM_mit_Python_07a_Input_Matplotlib_39_0.png

(Hat man in Spyder eingestellt, dass Plots “inline” d.h. direkt in der Konsole dargestellt werden sollen, dann wird bei Ausführen des Befehls plt.figure() lediglich dies hier angezeigt: <Figure size 640x480 with 0 Axes>.)

Axes

Das Axes-Objekt ist mehr oder weniger das Diagramm selbst. Genaugenommen ist es ein abegrenzter “Diagrammbereich” auf einem Figure-Objekt, in den Datenpunkte in Relation zu einer bestimmten Achsen-Skalierungen eingezeichnet werden können. Während das Figure-Objekt also die leere Leinwand ist, auf die prinzipiell etwas gezeichnet werden kann, ist das Axes-Objekt ein Koordinatensystem auf dieser leeren Leinwand, in die Datenpunkte in Relation zu einem Koordinatensystem eingezeichnet werden können.

Ein Axes-Objekt muss immer auf einem Figure-Objekt gezeichnet werden. Ein Figure-Objekt kann mehrere Axes beinhalten.

Axes-Objekte werden vom Figure-Objekt selbst erstellt, indem wir die Methode add_axes() eines existierenden Figure-Objektes anwenden. Unten erstelle ich mit dem Befehl leinwand = plt.figure() zunächst ein Figure-Objekt und weise es dem Variablennamen leinwand zu.

Dann wende ich die Figure-Methode add_axes() des eben erstellten Figure-Objektes leinwand an. Die Methode add_axes() erwartet eine Liste mit 4 Zahlen auf einer kontinuierlichen Skala zwischen 0 und 1. Sie beschreiben die Position und Größe des zu erstellenden Axes-Objekt auf dem Figure-Objekt.

Die Elemente der Input-Liste der Methode add_axes() im Überblick:

  1. X-Position der linken unteren Ecke des Axes-Objekt; Werte zwischen 0 und 1; 0 –> links; 0.5 –> mittig; 1 –> rechts

  2. Y-Position der linken unteren Ecke des Axes-Objekt; Werte zwischen 0 und 1; 0 –> unten; 0.5 –> mittig; 1 –> rechts

  3. relative Größe der X-Dimension des Axes-Objekt im Vergleich zur Größe der X-Dimension des Figure-Objektes; Werte zwischen 0 und 1; 0.5 –> halb so groß wie die X-Dimension des Figure-Objektes; 1–> Genau so groß wie die X-Dimension des Figure-Objektes

  4. relative Größe der Y-Dimension des Axes-Objekt im Vergleich zur Größe der Y-Dimension des Figure-Objektes; Werte zwischen 0 und 1; 0.5 –> halb so groß wie die Y-Dimension des Figure-Objektes; 1–> Genau so groß wie die Y-Dimension des Figure-Objektes

(Das ist auf jeden Fall verwirrend. Am besten du probierst selbst mal etwas mit unterschiedlichen Werten herum und erstellst verschiedene Figure- und Axes-Objekte. Später werden wir noch eine Methode kennenlernen, wo wir diese “komplizierten” Angaben nicht brauchen werden.)

Unten füge ich dem Figure-Objekt leinwand das Axes-Objekt diagrammbereich1 auf der leinwand-X-Position 0.1, der leinwand-Y-Position 0.1 und einer X-Dimensions-Größe von 80% und einer Y-Dimensionsgröße von 80%.

# Figure-Objekt erstellen
leinwand = plt.figure()

# Dem Figure-Objekt ein Axes-Objekt hinzufügen
diagrammbereich1 = leinwand.add_axes([0.1,0.1,0.8,0.8])

plt.show()
../_images/ABM_mit_Python_07a_Input_Matplotlib_41_0.png

Da man hier im Jupyter-Notebook das Figure-Objekt nicht getrennt vom Axes-Objekt sieht, hier das Ganze nochmal im Screenshot:

fig = plt.figure()
fig.set_size_inches(10,10)
plt.imshow(plt.imread('img/figure_axes.png'))
plt.show()
../_images/ABM_mit_Python_07a_Input_Matplotlib_43_0.png

Achtung: stellt man die Größe des Axes-Objektes zu groß ein, dann sieht man evtl. die Skalen-Markierungen nicht mehr (die Skalenmarkierungen zählen nicht zur eigentlichen Größe des Axes-Objekts).

In ein Figure-Objekt können prinzipiell beliebig viele, beliebig große Axes-Objekte an beliebigen Positionen eingefügt werden. Unten erstelle ich ein Figure-Objekt leinwand mit zwei Axes-Objekten diagrammbereich1 und diagrammbereich2.

leinwand = plt.figure()
diagrammbereich1 = leinwand.add_axes([0.1,0.1,0.8,0.8])
diagrammbereich2 = leinwand.add_axes([0.3,0.3,0.4,0.4])
plt.show()
../_images/ABM_mit_Python_07a_Input_Matplotlib_45_0.png

Nachdem man einen Diagrammbereich d.h. ein Axes-Objekt auf einem Figure-Objekt erstellt hat, kann man in diesen Diagrammbereich Datenpunkte einzeichnen. Für ein Liniendiagramm kann man dazu, die Methode plot() des jeweiligen Figure-Objektes verwenden. Unten erstelle ich zunächst zwei Beispiel-Datenreihen x_data und y_data, erstelle dann ein Figure- und ein Axes-Objekt und zeichne schließlich im Axes-Objekt die Datenpunkte der Datenreihen mithilfe der Methode plot() ein.

# Datenreihen
x_data = [1, 2, 3, 4, 5]
y_data = [1, 2, 4, 8, 16]

# Leinwand / Figure-Objekt erstellen
leinwand = plt.figure()

# Diagrammbereich / Koordinatensystem / Axes-Objekt auf Leinwand erstellen
diagrammbereich1 = leinwand.add_axes([0.1, 0.1, 0.8, 0.8])

# Datenpunkte / Linie in Diagrammbereich einzeichnen
diagrammbereich1.plot(x_data, y_data)

plt.show()
../_images/ABM_mit_Python_07a_Input_Matplotlib_47_0.png

Erstellen wir mehrere Diagrammbereiche (Axes) auf einer Leinwand (Figure), dann können wir in jedem Diagrammbereich jeweils unabhängig von den anderen Diagrammbereichen Datenpunkte einzeichnen:

# mehrere Datenreihen erstellen
x_data1 = [1, 2, 3, 4, 5]
y_data1 = [1, 2, 4, 8, 16]

x_data2 = [5, 4, 3, 2, 1]
y_data2 = [32, 64, 128, 256, 512]


# Leinwand / Figure-Objekt erstellen
leinwand = plt.figure()

# 2 Diagrammbereich auf Leinwand erstellen
diagrammbereich1 = leinwand.add_axes([0, 0, 0.4, 1])
diagrammbereich2 = leinwand.add_axes([0.5, 0, 0.4, 1])

# für beide Diagrammbereiche jeweils unterschiedliche Datenreihen plotten
diagrammbereich1.plot(x_data1, y_data1)
diagrammbereich2.plot(x_data2, y_data2)

plt.show()
../_images/ABM_mit_Python_07a_Input_Matplotlib_49_0.png

Übrigens: Ich verwende hier zu Demonstrationszwecken immer die Variablennamen leinwand und diagrammbereich. Natürlich kann man auch kürzere Namen wählen. Es hat sich eingebürgert ein Figure-Objekt unter dem Namen fig und ein Axes-Objekt unter dem Namen ax abzuspeichern.

Etwas einfacher: Axes und Figures mit plt.subplots() erstellen

Wir kennen jetzt die grundlegende Logik von Matplolib. Die getrennte Erstellung von Figure-Objekten und Axes-Objekten und die Anordnung von Letzterem auf dem Ersteren bietet uns die volle Kontrolle, aber es erscheint doch für die meisten Anwendungsfälle etwas unnötig kompliziert zu sein. So viel Kontrolle brauchen wir dann auch nicht. Ein gutes Mittelmaß zwischen Kontrolle und Komfort bietet uns die Funktion plt.subplots().

Die Funktion plt.subplots() ermöglicht es, gleichzeitig ein Figure-Objekt und ein oder mehrere Axes-Objekte zu erstellen und diese automatisch “perfekt” neben- oder übereinander anzuordnen.

Im einfachsten Fall gibt die Funktion plt.subplot() ein Figure-Objekt und ein Axes-Objekt zurück. Führt man die Funktion plt.subplots() einfach aus, ohne die Rückgabewerte Variablen zuzuweisen, dann bekommt man etwas kryptisch angezeigt, wie die Ausgabe der Funktion eigentlich aussieht:

plt.subplots()
(<Figure size 432x288 with 1 Axes>,
 <matplotlib.axes._subplots.AxesSubplot at 0x1cd13ef3108>)
../_images/ABM_mit_Python_07a_Input_Matplotlib_52_1.png

Die Funktion gibt ein Tupel - also so etwas wie eine Liste nur mit runden Klammern - mit zwei Objekten zurück:

(<Figure size 432x288 with 1 Axes>,  <matplotlib.axes._subplots.AxesSubplot at 0x2a7520de848>)

Das erste Objekt ist das Figure-Objekt, das zweite Objekt ist ein Axes-Objekt.

Wir könnten nun theoretisch auch das gesamte Tupel einer einzigen Variable zuweisen, ohne die darin entahltenen Objekte zu entpacken. Das wäre aber für die weitere Arbeit mit den Objekten bei der Diagrammerstellung recht unpraktisch, da wir dann per Indizierung jeweils auf die einzelnen Objekte zugreifen müssten. Daher wenden wir hier besser die Mehrfachzuweisung (siehe Exkurs 2) an und weisen das Figure-Objekt und das Axes-Objekt direkt jeweils einem Variablennamen zu:

leinwand, diagrammbereich = plt.subplots()
../_images/ABM_mit_Python_07a_Input_Matplotlib_54_0.png

Nachdem das Figure-Objekt und das Axes-Objekt auf diese Weise erstellt und abgespeichert wurde, können Datenpunkte innerhalb des erstellten Axes-Objektes eingezeichnet werden:

x_data = [1, 2, 3, 4, 5]
y_data = [1, 2, 4, 8, 16]

leinwand, diagrammbereich = plt.subplots()
diagrammbereich.plot(x_data, y_data)
[<matplotlib.lines.Line2D at 0x1cd13de6648>]
../_images/ABM_mit_Python_07a_Input_Matplotlib_56_1.png

Der größte Vorteil an der Verwendung der Funktion plt.subplots() ist nun, dass relativ unkompliziert mehrere Axes-Objekte d.h. “Diagrammbereiche” gleichzeitig erstellt und optimal auf dem Figure-Objekt d.h. auf der Leinwand angeordnet werden können. Dazu müssen wir per Funktions-Input angeben, wie viele Axes-Objekte jeweils übereinander und wie viele Axes-Objekte jeweils nebeneinander angeordnet werden sollen.

Wir geben also zwei Zahlen als Argumente in die Funktion plt.subplots() und bestimmen dadurch die Anzahl der Zeilen (übereinander) von Axes-Objekten und die Anzahl der Spalten (nebeneinander) von Axes-Objekten. Allgemein kann die Syntax so beschrieben werden: plt.suplots(ANZAHL_ZEILEN, ANZAHL_SPALTEN). Standardmäßig sind diese beiden Inputs auf 1 gesetzt, daher wurde oben auch nur ein Axes-Objekt erstellt.

Unten gebe ich eine 1 und eine 2 ein und sage somit, dass eine Zeile mit zwei Spalten an Diagrammbereichen erstellt werden sollen:

plt.subplots(1, 2)
(<Figure size 432x288 with 2 Axes>,
 array([<matplotlib.axes._subplots.AxesSubplot object at 0x000001CD13AEA308>,
        <matplotlib.axes._subplots.AxesSubplot object at 0x000001CD13DA3208>],
       dtype=object))
../_images/ABM_mit_Python_07a_Input_Matplotlib_58_1.png

Jetzt sage ich beispielhaft, dass 2 Zeilen und 2 Spalten Axes-Objekte erstellt werden sollen:

plt.subplots(2, 2)
(<Figure size 432x288 with 4 Axes>,
 array([[<matplotlib.axes._subplots.AxesSubplot object at 0x000001CD13A703C8>,
         <matplotlib.axes._subplots.AxesSubplot object at 0x000001CD105888C8>],
        [<matplotlib.axes._subplots.AxesSubplot object at 0x000001CD10497308>,
         <matplotlib.axes._subplots.AxesSubplot object at 0x000001CD13DC6188>]],
       dtype=object))
../_images/ABM_mit_Python_07a_Input_Matplotlib_60_1.png

(Die Axes-Objekte sind alle noch etwas dicht gedrängt. Das ändert sich aber, sobald wir Daten einzeichnen. Weiter unten werden wir auch lernen, wie wir das Figure-Objekt größer machen, damit alle Axes-Objekte genug Platz haben.)

Um die Diagrammbereiche auch mit Daten füllen zu können, müssen wir wieder auf die einzelnen Axes-Objekte zugreifen und z.B. mit der Methode plot() Linien einzeichnen. Dazu müssen wir auch hier wieder die Figure- und Axes-Objekte Variablennamen zuweisen.

Das Figure-Objekt können wir wie gewohnt zuweisen, doch bei den Axes-Objekten gibt es nun einen Unterschied, da diesmal nicht nur ein, sondern mehrere Axes-Objekte durch die Funktion plt.subplots() zurückgegeben werden. Wenn wir uns den Output oben mal genauer anschauen, dann sehen wir, dass die Axes-Objekte nun nochmals zusammen in einen Container gepackt wurden - einem Numpy-Array.

Uns braucht an dieser Stelle nicht genau interessieren was das ist. Wir müssen nur wissen, dass solange wir nur eine Zeile oder nur eine Spalte haben, ein Numpy-Array letztlich die gleiche Datenstruktur wie eine Liste aufweist. Wir können also ganz normal mittels Listen-Indizierung darauf zugreifen. Wenn wir mehr als eine Zeile und mehr als eine Spalte haben, dann weist das Numpy-Array die gleiche Struktur wie eine Liste von Listen auf. Wir können also per “doppelter” Listenindizierung - erst Zeile, dann Spalte - darauf zugreifen.

Unten erstelle ich ein Figure-Objekt und eine Zeile mit zwei Spalten an Axes-Objekten. Die beiden Axes-Objekte werden in einem eindimensionalen Numpy-Array d.h. praktisch in einer Liste ausgegeben.

Per Mehrfachzuweisung weise ich das Figure-Objekt der Variable leinwand und das Numpy-Array mit den beiden Axes-Objekten der Variable diagrammbereiche zu:

leinwand, diagrammbereiche = plt.subplots(1, 2)
../_images/ABM_mit_Python_07a_Input_Matplotlib_62_0.png

Die Variable diagrammbereiche ist also immer noch ein Datencontainer, in dem sich die beiden Axes-Objekte befinden:

diagrammbereiche
array([<matplotlib.axes._subplots.AxesSubplot object at 0x000001CD13AA0A48>,
       <matplotlib.axes._subplots.AxesSubplot object at 0x000001CD13A83FC8>],
      dtype=object)

Um nun auf ein Axes-Objekt aus diagrammbereiche Daten einzuzeichnen, greife ich auf dieses einfach per Indizierung zu und führe die gewohnte Methode plot() aus. Unten greife ich z.B. mit dem Index 0 auf das erste Element d.h. das erste Axes-Objekt aus diagrammbereiche d.h. das linke Diagramm zu und plotte die Daten.

# Daten erstellen
x_data = [1, 2, 3, 4, 5]
y_data = [1, 2, 4, 8, 16]

# Axes- und Figure-Objekte erstellen
leinwand, diagrammbereiche = plt.subplots(1, 2)

# Auf das erste Figure-Objekt aus *diagrammbereiche* zugreifen und Daten einzeichnen
diagrammbereiche[0].plot(x_data, y_data)

plt.show()
../_images/ABM_mit_Python_07a_Input_Matplotlib_66_0.png

Unten plotte ich nun auch in das rechte Diagramm Daten, indem ich zusätzlich noch mittels Index 1 das zweite bzw. das rechte Diagramm auswähle und eine Datenlinie mittels plot() einzeichne:

# Daten erstellen
x_data = [1, 2, 3, 4, 5]
y_data = [1, 2, 4, 8, 16]

x_data2 = [5, 4, 3, 2, 1]
y_data2 = [32, 64, 128, 256, 512]

# Axes- und Figure-Objekte erstellen
leinwand, diagrammbereiche = plt.subplots(1, 2)

# Auf das erste Figure-Objekt aus *diagrammbereiche* zugreifen und Daten einzeichnen
diagrammbereiche[0].plot(x_data, y_data)

# Auf das zweite Figure-Objekt aus *diagrammbereiche* zugreifen und Daten einzeichnen
diagrammbereiche[1].plot(x_data2, y_data2)

plt.show()
../_images/ABM_mit_Python_07a_Input_Matplotlib_68_0.png

Genauso funktioniert dies auch, wenn die Subplots nicht nebeneinander, sondern übereinander d.h. innerhalb eine Spalte angeordnet sind - auch hier haben wir nur eine Dimension und daher nur ein eindimensionales Numpy-Array:

# Daten erstellen
x_data = [1, 2, 3, 4, 5]
y_data = [1, 2, 4, 8, 16]

x_data2 = [5, 4, 3, 2, 1]
y_data2 = [32, 64, 128, 256, 512]

# Axes- und Figure-Objekte erstellen
leinwand, diagrammbereiche = plt.subplots(2, 1)

# Auf das erste Figure-Objekt aus *diagrammbereiche* zugreifen und Daten einzeichnen
diagrammbereiche[0].plot(x_data, y_data)

# Auf das zweite Figure-Objekt aus *diagrammbereiche* zugreifen und Daten einzeichnen
diagrammbereiche[1].plot(x_data2, y_data2)

plt.show()
../_images/ABM_mit_Python_07a_Input_Matplotlib_70_0.png

Doch Achtung, sobald die Funktion plt.subplots() sowohl mehr als eine Zeile als auch mehr als eine Spalte an Subplots erstellen soll, werden die Axes-Objekte nicht mehr nur in einem listenähnlichen eindimensionalen Numpy-Array zurückgegeben, sondern in einem mehrdimensionalen Numpy-Array d.h. einer Matrix d.h. praktisch einer Liste von Listen (siehe Exkurs 1).

Um dann auf ein bestimmtes Axes-Objekt zuzugreifen, müssen wir zunächst per Indizierung angeben, in welcher Zeile das Axes-Objekt liegt und dann nochmals per Indizierung angeben, in welcher Spalte das Axes-Objekt liegt.

Unten erstelle ich per plt.subplots() ein Figure-Objekt, auf dem sich eine 2x2-Matrix mit Diagrammbereichen befindet. Um auf die einzelnen Diagramme dieser Diagrammmatrix zuzugreifen, verwende ich die “zweifache” Indizierung und zeichne die Daten ein.

# mehrere Datenreihen erstellen
x_data1 = [1, 2, 3, 4, 5]
y_data1 = [1, 2, 4, 8, 16]
x_data2 = [5, 4, 3, 2, 1]
y_data2 = [32, 64, 128, 256, 512]


## Diagramm erstellen ##

# Figure-Objekt und 2x2 Axes-Objekte erstellen
leinwand, diagrammbereiche = plt.subplots(2, 2)

# Daten in die jeweiligen Axes-Objekte eintragen
diagrammbereiche[0][0].plot(x_data1, y_data1) # Diagramm oben links; 1. Zeile u. 1. Spalte
diagrammbereiche[0][1].plot(x_data2, y_data2) # Diagramm oben rechts; 1. Zeile u. 2. Spalte
diagrammbereiche[1][0].plot(x_data2, y_data1) # Diagramm unten links; 2. Zeile u. 1. Spalte
diagrammbereiche[1][1].plot(x_data1, y_data2) # Diagramm unten rechts; 2. Zeile u. 2. Spalte
[<matplotlib.lines.Line2D at 0x1cd140bfac8>]
../_images/ABM_mit_Python_07a_Input_Matplotlib_72_1.png

Für ein Video über das Plotten mit Matplotlib in der object-oriented-API-Variante, schau doch mal hier:

from IPython.display import YouTubeVideo
YouTubeVideo("lxvGh9RL0gw")

Diagramme aufhübschen

Bisher haben wir immer nur relativ “rohe” Diagramme ohne Beschriftung und sonstigen Schnick-Schnack erstellt. Im Folgenden werden kurz die wichtigsten Dinge, welche Diagramme aufhübschen und besser lesbar machen, vorgestellt. Alle Beispiele erfolgen anhand eines Figure-Objektes mit einem Axes-Objekt. Sind mehrere Axes-Objekte auf einem Figure-Objekt vorhanden, muss dementsprechend der Axes-spezifische Code auf die jeweiligen Axes-Objekte angepasst werden.

Diagramm-Titel und Achsen-Label

Wichtig für die Lesbarkeit von Diagrammen ist deren Betitelung und die Beschriftung der Achsen. Um diese hinzuzufügen, kann die Axes-Methode set() ausgeführt werden und den Parametern title, xlabel und ylabel die gewünschten Werte zugewiesen werden.

Unten erstelle ich zunächst wieder Beispieldaten, erstelle ein Figure- und ein Axes-Objekt, plotte die Daten und füge dann schließlich die Beschriftungen in Zeile 12 hinzu.

# Daten erstellen
days = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
infections = [1, 2, 3, 5, 8, 13, 21, 26, 29, 31, 32, 32, 31, 29, 26, 21, 13, 8, 5, 3]

# Figure- und Axes-Objekt erstellen
leinwand, diagrammbereich = plt.subplots()

# Daten einzeichnen
diagrammbereich.plot(days, infections)

# Diagramm-Titel und Achen-Label einfügen
diagrammbereich.set(xlabel="Tage", ylabel="Infektionen", title = "Verlauf Pandemie")

# Diagramm anzeigen
plt.show()
../_images/ABM_mit_Python_07a_Input_Matplotlib_76_0.png

Achsenbereiche festlegen

Manchmal kann es notwendig sein, den angezeigten Bereich der X- und Y-Achsen selbst zu bestimmen. Auch dies geht innerhalb der Methode set(). Die Parameter xlim und ylim nehmen eine Liste, welche das jeweilige Achsenminimum und das Achsenmaximum enthält, entgegen. Zur besseren Übersicht schreibe ich unten die Paramter der Methode set() untereinander.

# Daten erstellen
days = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
infections = [1, 2, 3, 5, 8, 13, 21, 26, 29, 31, 32, 32, 31, 29, 26, 21, 13, 8, 5, 3]

# Figure- und Axes-Objekt erstellen
leinwand, diagrammbereich = plt.subplots()

# Daten einzeichnen
diagrammbereich.plot(days, infections)

# Diagramm-Titel und Achen-Label einfügen
diagrammbereich.set(
    xlabel="Tage", 
    ylabel="Infektionen", 
    title = "Verlauf Pandemie",
    xlim = [0, 30],
    ylim = [0, 50],    
)

# Diagramm anzeigen
plt.show()
../_images/ABM_mit_Python_07a_Input_Matplotlib_78_0.png

Linienbeschriftung und Legende

Wenn man mehrere Datenreihen in ein Diagramm bzw. ein Axes-Objekt einzeichnen möchte, dann empfiehlt es sich auch hier die einzelnen Datenreihen zu bezeichnen und dann deren Bezeichnung in einer Legende anzuzeigen. Dafür kann in der Methode, mit welcher die Daten gezeichnet werden - in diesem Fall plot() - der Parameter label genutzt werden, um die gerade gezeichnete Datenreihe zu bezeichnen. Unten geschieht dies in den Zeilen 10 und 11. Schließlich muss noch die Legende angezeigt werden. Dies geschieht über die Axes-Methode legend() in Zeile 17.

# Daten erstellen
days = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
infections = [1, 2, 3, 5, 8, 13, 21, 26, 29, 31, 32, 32, 31, 29, 26, 21, 13, 8, 5, 3]
recoveries = [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 5, 8, 13, 21, 26, 29, 31, 32]

# Figure- und Axes-Objekt erstellen
leinwand, diagrammbereich = plt.subplots()

# Daten einzeichnen (mit Label)
diagrammbereich.plot(days, infections, label = "Infizierte")
diagrammbereich.plot(days, recoveries, label = "Geheilte")

# Diagramm-Titel und Achen-Label einfügen
diagrammbereich.set(xlabel="Tage", ylabel="Infektionen", title = "Verlauf Pandemie")

# Legende einzeichnen
diagrammbereich.legend()

# Diagramm anzeigen
plt.show()
../_images/ABM_mit_Python_07a_Input_Matplotlib_80_0.png

Gitter anzeigen

Um ein Gitter im Hintergrund des Plots anzuzeigen, kann die Axes-Methode grid() aufgerufen werden.

# Daten erstellen
days = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
infections = [1, 2, 3, 5, 8, 13, 21, 26, 29, 31, 32, 32, 31, 29, 26, 21, 13, 8, 5, 3]
recoveries = [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 5, 8, 13, 21, 26, 29, 31, 32]

# Figure- und Axes-Objekt erstellen
leinwand, diagrammbereich = plt.subplots()

# Daten einzeichnen
diagrammbereich.plot(days, infections, label = "Infizierte")
diagrammbereich.plot(days, recoveries, label = "Geheilte")

# Diagramm-Titel und Achen-Label einfügen
diagrammbereich.set(xlabel="Tage", ylabel="Infektionen", title = "Verlauf Pandemie")

# Legende einzeichnen
diagrammbereich.legend()

# Gitter einzeichnen
diagrammbereich.grid()

# Diagramm anzeigen
plt.show()
../_images/ABM_mit_Python_07a_Input_Matplotlib_82_0.png

Größe festlegen

Möchte man die Größe des erstellten Fensters, also dem Figure-Objekt, per Programmierung bestimmen, kann man die Methode set_size_inches() verwenden. Die Methode erwartet zwei Zahlen, welche die Höhe und Breite des Figure-Objektes in der Einheit Inch festlegen.

from matplotlib import pyplot as plt

# Daten erstellen
days = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
infections = [1, 2, 3, 5, 8, 13, 21, 26, 29, 31, 32, 32, 31, 29, 26, 21, 13, 8, 5, 3]
recoveries = [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 5, 8, 13, 21, 26, 29, 31, 32]

# Figure- und Axes-Objekt erstellen
leinwand, diagrammbereich = plt.subplots()

# Größe des Figure-Objektes verändern
leinwand.set_size_inches(10,10)

# Daten einzeichnen
diagrammbereich.plot(days, infections, label = "Infizierte")
diagrammbereich.plot(days, recoveries, label = "Geheilte")

# Diagramm-Titel und Achen-Label einfügen
diagrammbereich.set(
    xlabel="Tage", 
    ylabel="Infektionen", 
    title = "Verlauf Pandemie",  
)

# Legende einzeichnen
diagrammbereich.legend()

# Gitter einzeichnen
diagrammbereich.grid()

# Diagramm anzeigen
plt.show()
../_images/ABM_mit_Python_07a_Input_Matplotlib_84_0.png

imshow()

Für die einfache Darstellung von sogenannten Grids, also der zweidimensionalen Landschaften agentenbasierter Modellierungen (die Schachbrett-Landschaft) eignet sich in Matplotlib die Funktion bzw. Axes-Methode imshow(). Diese kann genau wie z.B. plot() oder scatter() als Methode eines Axes-Objektes ausgeführt werden, erwartet jedoch ein Matrix, akzeptiert wird u.a. auch eine einfach Liste-von-Listen-Matrix, als Input.

Im Folgenden erstelle ich zunächst eine 2x2-Matrix, welche Nullen und Einsen enthält. Dann erstelle ich wie gewohnt Figure- und Axes-Objekt mit plt.subplots() und wende schließlich, da wo oben meist plot() verwendet wurde, die Axes-Methode imshow() mit der 2x2-Matrix als Input an.

# Matrix erstellen
matrix = [
    [1,0],
    [0,1]
]

# Figure und Axes erstellen
leinwand, diagrammbereich = plt.subplots()

# imshow()-Methode des Axes-Objekt anwenden, Matrix ist Input
diagrammbereich.imshow(matrix)

plt.show()
../_images/ABM_mit_Python_07a_Input_Matplotlib_86_0.png

Wir sehen, die 2x2-Matrix wurde in ein Quadrat, welches aus 4 gleichgroßen, farbigen Quadraten besteht, übersetzt. Jedem Feld der Matrix entspricht einem Quadrat. Die Farbe des Quadrates ergibt sich aus dem Zahlenwert in der Matrix.

Unten ein weiteres Beispiel, diesmal eine 4x4-Matrix, welche die Zahlen 0, 1 und 2 beinhalten.

matrix = [
    [1,0,0,1],
    [0,2,2,0],
    [0,2,2,0],
    [1,0,0,1],
]

leinwand, diagrammbereich = plt.subplots()

diagrammbereich.imshow(matrix)

plt.show()
../_images/ABM_mit_Python_07a_Input_Matplotlib_88_0.png

Und natürlich könnte man hier auch wieder mehrere Subplots erstellen:

matrix1 = [
    [1,0],
    [0,1],
]

matrix2 = [
    [1,0,0,1],
    [0,2,2,0],
    [0,2,2,0],
    [1,0,0,1],
]

leinwand, diagrammbereich = plt.subplots(1,2)

diagrammbereich[0].imshow(matrix1)
diagrammbereich[1].imshow(matrix2)

plt.show()
../_images/ABM_mit_Python_07a_Input_Matplotlib_90_0.png

Wie kommen nun aber die Farben zustande? Jeder Zahl in der Matrix wird eine Farbe im Diagramm zugeordnet. Um festzulegen, welche Zahl welcher Farbe zugeordnet wird, greift die Methode imshow() auf eine sogenannte Colormap zurück. Hier findest du unten auf der Seite eine Übersicht über die standardmäßig verfügbaren Colormaps in Matplotlib.

Die in der Matrix verwendeten Zahlen werden als Zahlen eines kontinuierlichen Zahlenstrahls bzw. einer kontinuierlichen Skala angesehen. Das Minimum dieser Skala d.h. die niedrigste verwendete Zahl wird der Farbe am linken Ende der Colormap und das Maximum der Skala d.h. die höchste verwendete Zahl der Farbe am rechten Ende der Colormap zugeordnet. Die restlichen Skalenschritte werden dann gleichmäßig entlang der Colormap verteilt, sodass z.B. die Skalenmitte genau der mittleren Farbe der Colormap zugeordnet wird.

Oben hatten wir als erstes nur Nullen und Einsen in der Matrix verwendet, sodass wir praktisch eine Skala von 0 bis 1 hatten. Da die Methode imshow() die Colormap namens viridis als Standard-Colormap eingestellt hat, wurde die 0 der Farbe lila auf der linken Seite und die 1 der Farbe gelb auf der rechten Seite der Colormap viridis zugeordnet.

Im zweiten Beispiel hatten wir eine Skala von 0 bis 2, sodass die 0 als Skalenminimum der Farbe lila auf der linken Seite, die 1 als Skalenmitte der Farbe türkis in der Mitte und die 2 als Skalenmaximum der Farbe gelb auf der rechten Seite der Colormap viridis zugeordnet wurden.

Die in der Matrix verwendeten Skalen können beliebig groß und beliebig kleinschrittig sein. Im Folgenden erstelle ich z.B. eine Matrix mit 10 Zeilen und 10 Spalten und addiere in jeder Zelle der Matrix die Zeilen- und Spaltenposition.

# Anzahl der Zeilen und Spalten festlegen
n_rows = 10
n_cols = 10

# Oberliste für Matrix erstellen
matrix = []

# für die Anzahl der Zeilen
for i in range(n_rows):
    
    # Unterliste für Zeile erstellen
    zeile = []
    
    # für die Anzahl der Spalten
    for j in range(n_cols):
        
        # An die Zeile ein Element anhängen (In diesem Fall eine 0)
        zeile.append(i + j)
    
    # Zeile (Unterliste) an Matrix (Oberliste) anhängen
    matrix.append(zeile)

matrix
[[0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
 [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
 [2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
 [3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
 [4, 5, 6, 7, 8, 9, 10, 11, 12, 13],
 [5, 6, 7, 8, 9, 10, 11, 12, 13, 14],
 [6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
 [7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
 [8, 9, 10, 11, 12, 13, 14, 15, 16, 17],
 [9, 10, 11, 12, 13, 14, 15, 16, 17, 18]]
leinwand, diagrammbereich = plt.subplots()

diagrammbereich.imshow(matrix)

plt.show()
../_images/ABM_mit_Python_07a_Input_Matplotlib_93_0.png

Nun das ganze nochmal mit 100 Zeilen und Spalten, sodass wir eine feinere Auflösung bekommen:

# Anzahl der Zeilen und Spalten festlegen
n_rows = 100
n_cols = 100

# Oberliste für Matrix erstellen
matrix = []

# für die Anzahl der Zeilen
for i in range(n_rows):
    
    # Unterliste für Zeile erstellen
    zeile = []
    
    # für die Anzahl der Spalten
    for j in range(n_cols):
        
        # An die Zeile ein Element anhängen (In diesem Fall eine 0)
        zeile.append(i + j)
    
    # Zeile (Unterliste) an Matrix (Oberliste) anhängen
    matrix.append(zeile)

leinwand, diagrammbereich = plt.subplots()
diagrammbereich.imshow(matrix)
plt.show()
../_images/ABM_mit_Python_07a_Input_Matplotlib_95_0.png

Mithilfe des Parameters cmap können wir die verwendete Colormap ändern. Dafür müssen wir cmap die Bezeichnung der gewünschten Colormap zuweisen. Unten setze ich den Parameter cmap erst auf "Greys" und dann auf "Dark2":

leinwand, diagrammbereiche = plt.subplots(1, 2)
diagrammbereiche[0].imshow(matrix, cmap = "Greys")
diagrammbereiche[1].imshow(matrix, cmap = "Dark2")
plt.show()
../_images/ABM_mit_Python_07a_Input_Matplotlib_97_0.png

Wie gesagt, die Methode imshow() nimmt (ohne weitere Einstellung) den kleinsten vorhandenen Wert als Skalenminimum und den größten vorhandenen Wert als Skalenmaximum an. Wir können aber auch selbst das Skalenminimum und -maximum festlegen. Das ist sinnvoll, wenn wir über mehrere Plots hinweg die Farbzuweisungen gleich halten wollen, obwohl sich in den verschiedenen Plots evtl. die kleinsten und größten Werte voneinander unterscheiden (was v.a. auch bei der Animation solcher Plots wichtig ist).

Das Skalenminimum und Skalenmaximum stellen wir mit den beiden Parametern vmin und vmax ein. Unten verändert sich in jeder Matrix das Skalenmaximum. Da das Skalenminimum und Skalenmaximum per vmin und vmax festgelegt ist, bleiben die Farben den Zahlen fest zugeordnet.

matrix1 = [
    [1,0,0],
    [1,0,0],
    [1,0,0],
]

matrix2 = [
    [1,2,0],
    [1,2,0],
    [1,2,0],
]

matrix3 = [
    [1,2,3],
    [1,2,3],
    [1,2,3],
]

leinwand, diagrammbereiche = plt.subplots(1, 3)

diagrammbereiche[0].imshow(matrix1, vmin = 0, vmax = 3)
diagrammbereiche[1].imshow(matrix2, vmin = 0, vmax = 3)
diagrammbereiche[2].imshow(matrix3, vmin = 0, vmax = 3)

plt.show()
../_images/ABM_mit_Python_07a_Input_Matplotlib_99_0.png

Übrigens akzeptiert die Methode imshow() auch Matrizen, deren Elemente nicht einzelne Zahlen sind, sondern sogenannte RGB-Farbcodes darstellen. Dadurch steckt die Information über die anzuzeigende Farbe im Element selbst und muss nicht erst über eine Colormap zugeordnet werden.

“RGB” steht für “rot grün blau”. Ein RGB-Farbcode definiert eine bestimmte Farbe, indem angegeben wird, wie viel rot, wie viel grün und wie viel blau in dieser Farbe steckt (jede Farbe besteht letztlich aus diesen drei Farben). Wie viel von einer Farbe in den Farbmix kommt, wird über eine Zahl zwischen 0 und 255 angegeben. Ein RGB-Farbcode besteht daher aus drei Zahlen zwischen 0 und 255. Die erste Farbe steht für die “Menge” an rot, die zweite Zahl für die “Menge” an grün und die dritte Zahl für die “Menge” an blau.

Ein reines rot hat dementsprechend den RGB-Code “255 0 0”. Ein reines grün hat den Code “0 255 0” und ein reines blau den Code “0 0 255”. Die meisten Farben sind jedoch eine Mischung aus allem. Hier findest du eine Übersicht über gängige Farben und deren RGB-Farbcodes.

In Python gibt man RGB-Farbcodes meist als eine Liste oder ein Tupel mit den entsprechenden 3 Zahlen an z.B. so: [255, 100, 0].

Unten speichere ich zunächst zwei RGB-Farbcodes unter den Namen gold und navy, erstelle dann eine Matrix mit diesen Farben und lasse die Matrix mit imshow() anzeigen.

gold = [255, 215, 0]
navy = [0, 0, 128]

rgb_matrix = [
    [gold, navy, gold],
    [navy, gold, navy],
    [gold, navy, gold],
]

leinwand, diagrammbereich = plt.subplots()
diagrammbereich.imshow(rgb_matrix)
plt.show()
../_images/ABM_mit_Python_07a_Input_Matplotlib_101_0.png

Schellings Modell der Segregation

Im Folgenden programmieren wir eine Variante des sogenannten Segregrationsmodell von Schelling. Dieses wird so ziemlich in jeder Einführung in die agentenbasierte Modellierung in den Sozialwissenschaften erwähnt und wurde auch in diesem Lehrskript im ersten Kapitel kurz behandelt. Einen Original-Text von Schelling über das Segregationsmodell findest du z.B. hier.

Das Grundmodell geht so: Auf einer zweidimensionalen Landschaft (dem sogenannten Grid), die in viele gleichgroße Quadrate eingeteilt ist (auch Zellen oder Felder genannt), “wohnen” Agenten zweier unterschiedlicher Gruppen. Jeder Agent bewohnt dabei genau ein Feld. Jeder Agent hat das Bedürfnis, dass in der direkten Nachbarschaft d.h. in den 8 Zellen um die eigene Zelle (der sogenannten Moore-Umgebung) die eigene Gruppe mindestens einen bestimmten Anteil der Gesamtzahl an Agenten ausmacht. Falls der Anteil der eigenen Gruppe in der Nachbarschaft zu gering ist, dann sucht der Agent nach einem neuen Wohntort und zieht um.

Das Modell soll zeigen, dass eine stark segregierte Landschaft d.h. deutlich voneinander abgegrenzbare Wohngebiete, in denen entweder nur die eine oder nur die andere Gruppe wohnt, bereits bei einem geringen Bedürfnis nach Nachbarn der eigenen Gruppe (Homophilie) entsteht. Die unterschiedlichen Gruppen können beispielsweise für verschiedene soziale Milieus oder unterschiedliche ethnische Gruppen stehen. Schelling selbst nimmt vor allem die getrennten Wohngebiete der schwarzen und weißen Bevölkerung in den USA als Beispiel.

Hier im Video siehst du eine Animation des Segregationsprozesses des Modells:

YouTubeVideo("dnffIS2EJ30")

Wir werden in diesem Kapitel die einfachst mögliche Variante dieses Modells programmieren und einzelne Zustände des Grids per Matplotlib visualisieren. Das Grid werden wir dabei als eine Matrix, die als Liste von Listen implementiert ist, repräsentieren. Jede Zelle in einer bestimmten Zeile und einer bestimmte Spalte der Matrix entspricht einer bestimmten Position auf dem Grid. Jedes Element dieser Matrix ist eine Zahl, die entweder für eine leere Zelle d.h. einen unbewohntes Feld, einen Agenten der Gruppe1 oder der Gruppe2 steht. Für die graphische Darstellung werden die Matrix in die Axes-Methode imshow() einspeisen, wobei dann jede Zahl in eine Farbe übersetzt wird.

Im Folgenden werden wir wieder Schritt für Schritt das Modell programmieren. Unten gibt es dafür auch wieder eine Code-Schablone. Prinzipiell steht es dir aber offen, ob du dich an das Vorgehen in den folgenden Aufgaben hälst. Falls du dir es zutraust, kannst du auch probieren, komplett eigenständig das Schellingmodell nachzuprogrammieren oder auch bei einzelnen Teilschritten eigene Lösungen ausprobieren. Was zählt ist, dass am Ende die Ergebnisse d.h. die finalen Landschaften verschiedener Modelldurchläufe mit unterschiedlichen Nachbarschaftstoleranzwerten der Agenten per Matplotlib angezeigt werden (Aufgabe 15).

Hier die Schritt-für-Schritt-Anleitung:

1. Zu Beginn müssen wir uns überlegen, durch welche Zahlen die 3 Zellzustände (leer, Agent der Gruppe1, Agent der Gruppe2) repräsentiert werden. Da wir es möglichst einfach haben wollen, sollten wir Zahlen wählen, die direkt sinnvoll mittels Colormap in Farben übersetzt werden können (wir könnten auch irgendwelche Symbole zur Repräsentation der drei Zustände nehmen und vor der graphischen Darstellung diese in RGB-Farbcodes umwandeln, das ist aber an dieser Stelle unnötig kompliziert). Du kannst hier gerne beliebige Zahlen und später auch eine beliebige Colormap wählen, solange du weißt, welche Farbe/Zahl für welche/n Gruppe/Zustand steht.

Ich würde z.B. vorschlagen, eine Colormap aus dem Bereich “Diverging Colormaps” mit der Farbe weiß in der Mitte zu wählen. Dann könnte man die z.B. die Gruppe1 mit dem Skalenminimum, Gruppe2 mit dem Skalenmaximum und leere Felder mit der Skalenmitte (weiß) repräsentieren. Eine einfache Zuordnung wäre dann z.B. der Wert 1 für Gruppe1, der Wert 2 für leere Felder und der Wert 3 für Gruppe2 (1 = Skalenminimum, 2 = Skalenmitte, 3 = Skalenmaximum). Es kann aber verwirrend sein, wenn z.B. Gruppe2 mit einer 3 repräsentiert wird. Ich persönlich habe daher z.B. einfach 7, 8 und 9 verwendet.

  • Überlege dir, mit welchen Zahlen du leere Felder, Gruppe1 und Gruppe2 repräsentieren möchtest und welche Colormap du verwenden möchtest. (Hier musst du noch nichts coden, es geht allein darum dir zu überlegen, in welchen Farben und mit welchen Zahlen du die Agenten später darstellen möchtest, damit du in den kommenden Aufgaben die entsprechenden Zahlen als Repräsentation für die verschiedenen Zellen/Agenten verwenden kannst.)

  • Schreibe dir deine Repräsentation von leeren Feldern, Agenten der Gruppe1 und Agenten der Gruppe2 als Kommentar in deinen Code damit du später nicht durcheinander kommst. (Profis würden sich die enstsprechenden Zuordnungen einfach als Variablen am Beginn des Programms abspeichern und dann später nur noch die Variablennamen verwenden. Das hat den Vorteil, dass man so die Zuordnungen bei Bedarf schnell ändern kann.)

2. Zu Beginn einer Simulation müssen wir das Grid als Liste von Listen erstellen und mit leeren Feldern, Agenten der Gruppe1 und Agenten der Gruppe2 “befüllen”. Für die Erstellung einer Matrix verwenden wir am besten eine eigene Funktion. Die Wohnorte der Agenten sollten zu Beginn zufällig sein, also sollten die Elemente, welche leere Zellen, Agenten der Gruppe1 und Gruppe2 zufällig in der Matrix verteilt sein. Zugleich sollten wir kontrollieren können, wie viele Agenten von Gruppe1 und Gruppe2 generell auf dem dem Grid sind.

  • Schreibe eine Funktion namens create_grid(), welche eine Matrix als Liste von Listen erstellt und ausgibt. Die Matrix sollte 50 Zeilen und 50 Spalten aufweisen (die Größe darf aber auch per Parameter kontrollierbar sein). Die Elemente der Matrix sollten aus den drei Zahlen, welche leere Zellen, Agenten der Gruppe1 und Agenten der Gruppe2 repräsentieren, bestehen. Deren Position in der Matrix sollte zufällig sein. Der Anteil an Agenten auf dem Grid sollte (mehr oder weniger) 80% betragen (darf aber auch per Parameter kontrollierbar sein).

Dies ist gar keine leichte Aufgabe und es gibt viele Lösungen für dieses Problem. Mein Tipp für eine einfache, nicht perfekte, aber zufriedenstellende quick-and-dirty-Lösung wäre, die Matrix wie in “Exkurs 1” per For-Loop zu erstellen, aber die Elemente der Matrix beim Erstellen jeweils zufällig aus einer Liste zu ziehen, die die drei Zahlen in der gewünschten Häufigkeitsverteilung enthält. Möchte man z.B. ungefähr 80% des Grids mit Agenten der Gruppe1 und Gruppe2 gleichmäßig besiedeln, dann könnte man die Elemente der Matrix mittels random.choice() zufällig aus der Liste [7,7,7,7,8,8,9,9,9,9] (7 = Gruppe1, 8 = leer, 9 = Gruppe2) ziehen. Dadurch erhält man ungefähr die gewünschte Verteilung der Agenten mit einer zufälligen Positionierung auf dem Grid. Es gibt aber auch Lösungen, wie man exakt die gewünschte Besiedelung durch die Gruppen/Agenten auf dem Grid erhält.

3. Auch die eigentliche Simulation würde ich in einer Funktion verpacken, sodass diese einfach öfter ausgeführt werden kann. Als Input sollte die Funktion mindestens den Anteil an Nachbarn der eigenen Gruppe, den die Agenten mindestens in der eigenen Umgebung haben möchten, aufnehmen.

  • Schreibe den Funktionskopf der Funktion und lege die Input-Parameter fest. Nenne die Funktion schelling().

4. Im ersten Schritt der Simulation muss das Grid erstellt werden und unter einem Variablennamen abgespeichert werden.

  • Schreibe Code, der das Grid mithilfe der Funktion create_grid() erstellt und speichere es unter dem Namen grid ab.

5. Nachdem das initiale Grid erstellt ist, kann der Simulationsloop, welcher natürlich ein For-Loop ist, losgehen. Jede Runde des “Simulationsloops” stellt einen Zeitschritt dar.

  • Schreibe den Kopf eines For-Loops, welcher entweder fest 500000 Iterationen lang läuft oder durch die Verwendung eines Input-Parameters der Funktion schelling() variabel lange läuft.

6. In jedem Zeitschritt wird genau ein Agent, der seine Nachbarschaft evaluieren und evtl. umziehen soll, zufällig ausgewählt. Wir ziehen diesen zufälligen Agenten indem wir zufällig einen Zeilen-Index und zufällig einen Spalten-Index des Grids auswählen.

  • Schreibe Code, der einen zufälligen Zeilenindex aus dem zulässigen Bereich zieht und diesen unter dem Namen row abspeichert.

  • Schreibe Code, der einen zufälligen Spaltenindex aus dem zulässigen Bereich zieht und diesen unter dem Namen col abspeichert.

(Mit “zulässigem Bereich” sind hier die Indizes, die die entsprechende Matrix auch aufweist, gemeint.)

7. Da wir eine komplett zufällige Position (zufällige Zeile & zufällige Spalte) auswählen, kann es sein, dass auf dieser Position gar kein Agent sitzt, sondern diese Position des Grids eine leere Zelle ist. Daher müssen wir evaluieren, inwiefern sich auf dieser zufällig gezogenen Position auf dem Grid auch wirklich ein Agent befindet und nicht nur eine leere Zelle. Nur wenn es sich um einen Agenten handelt, sollen die in den folgenden Aufgaben beschriebenen Handlungen der Agenten ausgeführt werden.

  • Schreibe eine (bzw. den Kopf einer) If-Bedingung, welche evaluiert, ob sich in der Zeile row und in der Spalte col in der Matrix grid ein Agent befindet. (Tipp: du musst dabei u.a. auf das Element grid[row][col] zugreifen.)

8. Nachdem wir nun eine zufällige Position ausgewählt haben und nachgeguckt haben, ob da ein Agent sitzt, der potentiell handeln kann, können wir diesen Agenten handeln lassen. Die Handlung eines Agenten besteht im Segretationsmodell nach Schelling grob aus zwei Teilen. Zum einen muss der Agent sich in seiner Nachbarschaft umgucken und diese auf die Gruppenzugehörigkeit seiner Nachbarn untersuchen. Zum anderen muss der Agent, wenn in der Nachbarschaft zu viele Agenten der fremden Gruppe sind, nach einer neuen Position Ausschau halten und eventuell umziehen.

Wir beginnen hier zunächst damit, dass der Agent sich in der Nachbarschaft umguckt. Dafür muss jeder Agent auf die 8 Zellen in dessen Umgebung zugreifen. Ich habe dir dafür die Funktion get_moore_neighbors() bereits vordefiniert. Diese Funktion nimmt die Position des aktuellen Agenten sowie das Grid als Input und gibt eine Liste der 8 den Agenten umgebenden Zellen zurück. Werden die 8 umgebenden Zellen als Nachbarschaft angesehen, nennt man dies übrigens auch “Moore-Umgebung”.

  • Versuche die Funktion get_moore_neighbors() zu verstehen.

  • Erstelle eine Liste der Moore-Nachbarschaft des Agenten mithilfe der Funktion get_moore_neighbors(). Nenne diese Liste my_neighborhood.

9. Der Agent verfügt nun über eine Liste seiner direkten Umgebung. Nun muss er evaluieren, welches Verhältnis von Mitgliedern seiner eigenen Gruppe zu Mitgliedern einer anderen Gruppe in dessen Nachbarschaft besteht. Dabei gehen wir in dieser Variante des Modells davon aus, dass leere Felder in der Umgebung/Nachbarschaft des Agenten nicht mitzählen. Der gewünschte Anteil nach Nachbarn der eigenen Gruppe bezieht sich also nicht direkt auf die 8 Felder in seiner Umgebung, sondern nur auf die vorhandenen Nachbarn auf diesen 8 Feldern in seiner Umgebung. Daher müssen wir gleich nicht nur die Agenten der eigenen Gruppe zählen, sondern auch wie viele Agenten insgesamt sich in der eigenen Nachbarschaft befinden, um später den Anteil der eigenen Gruppe in der Nachbarschaft richtig zu berechnen.

Ich schlage dafür Folgendes vor: Wir gehen per For-Loop die Liste der Nachbarschaft durch und zählen zum einen, wie viele Agenten der eigenen Gruppe sich in der Nachbarschaft befinden und zählen zum anderen, wie viele Agenten d.h. nicht-leere Felder sich insgesamt in der eigenen Nachbarschaft befinden. Mithilfe dieser beiden Zahlen können wir dann berechnen, wie hoch der Anteil der eigenen Gruppe in der Nachbarschaft (ohne leere Zellen) ist.

  • Initialisiere eine Variable namens n_agents_of_my_group und eine Variable n_agents und setze ihre Werte auf 0.

Mit diesen beiden Variablen zählen wir im Folgenden die Anzahl der Agenten der eigenen Gruppe in der Nachbarschaft und die Gesamtzahl der Agenten in der Nachbarschaft.

  • Starte einen For-Loop, welcher jedes Element der Liste my_neighborhood durchgeht. Wenn es sich um einen Agenten und nicht um eine leere Zelle handelt, dann erhöhe die Variable n_agents um den Wert 1. Wenn es sich, unabhängig von der vorhergehenden Bedingung, um einen Agenten der eigenen Gruppe handelt, dann erhöhe die Variable n_agents_of_my_group ebenfalls um den Wert 1.

Nach dem For-Loop wissen wir nun, wie viele Agenten generell in der Umgebung des aktuell behandelten Agenten sind und wie viele davon zur selben Gruppe des Agenten gehören.

10. In einem nächsten Schritt können wir nun den Anteil der Agenten der eigenen Gruppe an den Agenten in der Nachbarschaft ausrechnen. Z.B. so: share_of_my_group = n_agents_of_my_group / n_agents. Aber vorsicht: wenn ein Agent allein von leeren Zellen umgeben ist, dann wird bei der Berechnung des Anteils durch 0 geteilt. Um das zu verhindern, müssen wir zunächst wieder eine If-Bedingung vorschalten.

  • Schreibe eine If-Bedingung, die evaluiert, ob sich in der Nachbarschaft nicht nur leere Zellen befinden

  • Wenn sich nicht nur leere Zellen in der Nachbarschaft befinden, dann berechne den Anteil der Agenten der eigenen Gruppe wie folgt: share_of_my_group = n_agents_of_my_group / n_agents.

(Durch unsere Implementierung gehen wir praktisch davon aus, dass ein Agent, nur wenn dieser auch Nachbarn hat, möchte, dass ein bestimmter Anteil dieser Nachbarn zu seiner eigenen Gruppe gehört. Hat ein Agent keine Nachbarn, dann hat er auch nicht das Bedürfnis neben Nachbarn seiner eigenen Gruppe zu wohnen.)

11. Nun kann der Agent evaluieren, inwiefern ihm der Anteil an eigenen Leuten in der Nachbarschaft ausreicht. Da dies und auch fast der gesamte folgende Code nur ausgeführt werden soll, wenn der Agent überhaupt eine Nachbarschaft aus Agenten und nicht nur aus leeren Zellen hat, bleiben wir mit unserem Code innerhalb der If-Bedingung der vorherigen Aufgabe.

  • Schreibe eine If-Bedingung, welche evaluiert, ob die Nachbarschaft des Agenten einen kleineren Anteil an Agenten der eigenen Gruppe aufweist als der Agent mindestens möchte. Der Grenzwert sollte ja als Parameter in die Funktion schelling() eingegeben werden.

12. Wenn der Anteil an Nachbarn der eigenen Gruppe an der Anzahl aller Nachbarn kleiner ist als der Grenzwert, dann möchte der Agent umziehen. Um es einfach zu halten, schlage ich folgende Implementierung des Umzugs vor: Wir ziehen zunächst zufällige Zeilen- und Spaltenindizes und prüfen dann, inwiefern die Position auf dem Grid leer ist. Wenn diese leer ist, dann zieht der Agent dort hin, unabhängig davon, welche Nachbarschaft dort auf ihn wartet. Wenn die Position hingegen besetzt ist, dann zieht der Agent einfach nicht um.

  • Schreibe Code, der einen zufälligen Zeilen-Index aus dem zulässigen Bereich zieht und diesen unter dem Namen new_row abspeichert.

  • Schreibe Code, der einen zufälligen Spalten-Index aus dem zulässigen Bereich zieht und diesen unter dem Namen new_col abspeichert.

  • Schreibe eine If-Bedingung, die überprüft, ob es sich bei dieser zufällig gezogenen Position auf dem Grid um eine leere Zelle handelt.

  • Falls es sich um eine leere Zelle handelt, dann setze den Agenten auf diese Position des Grids und ersetze die vorherige Position des Agenten durch eine leere Zelle (!).

13. Wir haben nun alle Handlungsregeln des Segregationsmodells eingebaut. Jetzt muss nur noch das grid am Ende der Funktion schelling() ausgegeben werden. Dieses soll natürlich erst ausgegeben werden, wenn die gesamte Simulation durchgelaufen ist.

  • Gib das grid am Ende der Funktion aus. Achte auf das richtige Einrücklevel.

14. Die Funktion schelling() ist fertig und wir können diese zum ersten mal laufen lassen. Wahrscheinlich bekommst du jetzt noch mehrere Fehlermeldungen ausgegeben. Das ist ganz normal und liegt v.a. auch daran, dass wir eine sehr lange Funktion schreiben, aber diese erst am Ende testen können. Im Folgenden soll die ausgegebene Matrix/die ausgegebene Landschaft der Funktion schelling() mittels imshow() angezeigt werden.

  • Lasse die Funktion schelling() laufen, speichere das Ergebnis/Grid unter einem Variablennamen ab und plotte dieses mithilfe von Matplotlib und imshow(). Denk daran, nun die richtige Colormap zu verwenden. Außerdem musst du darauf achten, dass du die Simulation lange genug laufen lässt, damit z.B. ein Segregationseffekt sichtbar wird.

15. Um ein Gefühl dafür zu bekommen, welcher Toleranzgrenzwert zu welcher Segregation führt, sollten wir die Simulation mit verschiedenen Toleranzwerten ausführen und die Ergebnisse mittels imshow() anzeigen lassen. Hier eignet sich natürlich die Möglichkeit, mehrere Subplots in einem Figure-Objekt darstellen zu können, hervorragend, um die Ergebnisse zu vergleichen.

  • Lasse die Funktion schelling() mit unterschiedlichen “Toleranzen” der Agenten für die eigene Nachbarschaft laufen und speichere deren Ergebnisse unter verschiedenen Variablennamen

  • Erstelle eine Graphik, in der du die unterschiedlichen Ergebnisse für die unterschiedlichen Grenzwerte gleichzeitig darstellst.

Code-Schablone

import random
from matplotlib import pyplot as plt

def get_moore_neighbors(row, col, grid):
    
    """
    Sucht die 8 umgebenden Zellen der sogenannten Moore-Nachbarschaft heraus und gibt diese in einer Liste aus.
    
    Das Grid wird (mittels Modulo) als Torus behandelt d.h. das rechte Ende der Matrix schließt an das
    linke Ende der Matrix und das obere Ende der Matrix schließt an das untere Ende der Matrix an.
    
    row: Zeilen-Index des behandelten Agenten/der behandelten Zelle
    col: Spalten-Index des behandelten Agenten/der behandelten Zelle
    grid: Grid/Matrix als Liste von Listen; Beinhaltet die Agenten/Zellen auf unterster Ebene
    
    """
    
    # Anzahl der Zeilen in Grid/Matrix messen
    n_rows = len(grid)
    
    # Anzahl der Spalten in Grid/Matrix messen, indem die Anzahl der "Spalten" in der ersten Zeile gemessen wird
    n_cols = len(grid[0])
    
    # die 8 umgebenden Nachbarn heraussuchen und in Liste speichern
    neighbors = [
            grid[(row-1)%n_rows][(col-1)%n_cols], # oben-links
            grid[(row-1)%n_rows][col],            # oben
            grid[(row-1)%n_rows][(col+1)%n_cols], # oben-rechts
            
            grid[row][(col-1)%n_cols],            # links
            grid[row][(col+1)%n_cols],            # rechts

            grid[(row+1)%n_rows][(col-1)%n_cols], # unten-links
            grid[(row+1)%n_rows][col],            # unten
            grid[(row+1)%n_rows][(col+1)%n_cols], # unten-rechts
        ]
    
    # Liste von Nachbarn ausgeben
    return neighbors



# Aufgabe 3
# Funktionskopf der Funktion *schelling()*

    
    # Aufgabe 4
    # Grid initial erstellen
    
    
    # Aufgabe 5
    # für jeden Zeitschritt
    
        
        # Aufgabe 6
        # Zufällige Zeile wählen
        
        
        # Zufällige Spalte wählen
        
        
        # Aufgabe 7
        # Wenn das Element keine leere Zelle ist (sondern Agent)
        
            
            # Aufgabe 8
            # Nachbarn des Agenten heraussuchen und in Liste abspeichern
           
            
            # Aufgabe 9
            # Zählvariable für Nachbarn d.h. nicht-leere Zellen in Nachbarschaft auf 0 setzen
            
            
            # Zählvariable für Nachbarn der selben Gruppe auf 0 setzen
            
            
            # für jedes Element in Nachbarschaft
            
                
                # wenn Nachbar Agent bzw. keine leere Zelle ist
                
                    # Zählvariable für nicht-leere Zellen in Nachbarschaft 1 erhöhen
                    
                
                # wenn Nachbar in selber Gruppe wie Agent
                
                    # Zählvariable für Nachbarn der selben Gruppe 1 erhöhen
                    
                
            
            # Aufgabe 10
            # wenn Agent nicht nur von leeren Zellen umgeben ist
            
                
                # Anteil der eigenen Gruppe in Nachbarschaft berechnen
                
                
                # Aufgabe 11
                # wenn der Anteil der Nachbarn der eigenen Gruppe an allen Nachbarn (ohne leere Zellen) 
                # kleiner Grenzwert ist
                
                    
                    # Aufgabe 12
                    # Zufällige neue Zeile wählen
                    
                    
                    # Zufällige neue Spalte wählen
                    
                    
                    # Wenn auf neuer Position eine leere Zelle ist
                    
                        
                        # Agent auf neue Position setzen
                        
                        
                        # Auf alter Position eine leere Zelle einfügen
                        
    
    # Aufgabe 13
    # Grid ausgeben
    


# Aufgabe 14 & 15

Im nächsten Kapitel schauen wir uns u.a. an, wie wir ein solches Segregationsmodell wie unten in Matplotlib animieren können.

HTML(anim.to_jshtml())